package inline;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import com.bytedance.common.plugin.framework.so.PluginLibExtractor;
import com.bytedance.common.plugin.framework.util.PluginUtil;
import com.bytedance.common.utility.Logger;

import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

public class PluginExtractor {

    public static void closeQuietly(Closeable closeable) {
        try {
            closeable.close();
        } catch (IOException ioe) {
            Log.w("PluginExtractor", "Failed to close resource", ioe);
        }
    }

    public static void extractDex2Zip(ZipFile apkZipFile, ZipEntry classesZipEntry, File dexOutputFile, String dexFileName) throws IOException {
        ZipOutputStream zipOutputStream;
        InputStream inputStream = apkZipFile.getInputStream(classesZipEntry);
        File tempOutputFile = File.createTempFile(dexFileName, ".zip", dexOutputFile.getParentFile());
        Log.i("PluginExtractor", "Extracting " + tempOutputFile.getPath());
        try {
            zipOutputStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tempOutputFile)));
            ZipEntry zipEntry = new ZipEntry("classes.dex");
            zipEntry.setTime(classesZipEntry.getTime());
            zipOutputStream.putNextEntry(zipEntry);
            byte[] buffer = new byte[16384];
            int read;
            for (read = inputStream.read(buffer); read != -1; read = inputStream.read(buffer)) {
                zipOutputStream.write(buffer, 0, read);
            }
            zipOutputStream.closeEntry();
            zipOutputStream.close();
            Log.i("PluginExtractor", "Renaming to " + dexOutputFile.getPath());
            if (tempOutputFile.renameTo(dexOutputFile)) {
                return;
            } else {
                throw new IOException("Failed to rename \"" + tempOutputFile.getAbsolutePath() + "\" to \"" + dexOutputFile.getAbsolutePath() + "\"");
            }
        } catch (Throwable throwable) {

        } finally {
            try {
                PluginExtractor.closeQuietly(inputStream);
                tempOutputFile.delete();
            } catch (Exception e) {

            }
        }
    }

    public static void deleteCorruptDexFiles(File dexDir, final String dexFileName) throws IOException {
        dexDir.mkdir();
        if (!dexDir.isDirectory()) {
            throw new IOException("Failed to create dex directory " + dexDir.getPath());
        }

        File[] dexFiles = dexDir.listFiles(new FileFilter() {
            @Override
            public final boolean accept(File pathname) {
                return !pathname.getName().startsWith(dexFileName);
            }
        });
        if (dexFiles == null) {
            Log.w("PluginExtractor", "Failed to list plugin dex dir content (" + dexDir.getPath() + ").");
        } else {
            int length = dexFiles.length;
            for (int i = 0; i < length; ++i) {
                File dexFile = dexFiles[i];
                Log.i("PluginExtractor", "Trying to delete old file " + dexFile.getPath() + " of size " + dexFile.length());
                if (!dexFile.delete()) {
                    Log.w("PluginExtractor", "Failed to delete old file " + dexFile.getPath());
                } else {
                    Log.i("PluginExtractor", "Deleted old file " + dexFile.getPath());
                }
            }
        }
    }

    public static List<File> extractDex2ZipList(File apkFile, File dexOutputDir) throws IOException {
        String dexFileName = apkFile.getName() + ".classes";
        PluginExtractor.deleteCorruptDexFiles(dexOutputDir, dexFileName);
        ArrayList outputDexZipList = new ArrayList();
        ZipFile apkZipFile = new ZipFile(apkFile);
        try {
            ZipEntry classesZipEntry = apkZipFile.getEntry("classes.dex");
            int index = 1;
            while (true) {
                if (classesZipEntry == null) {
                    break;
                }
                File outputDexZipFile = new File(dexOutputDir, dexFileName + index + ".zip");
                outputDexZipList.add(outputDexZipFile);
                Log.i("PluginExtractor", "Extraction is needed for file " + outputDexZipFile);
                int retry = 0;
                boolean isValidZipFile = false;
                while (true) {
                    if (retry < 3 && !isValidZipFile) {
                        retry++;
                        PluginExtractor.extractDex2Zip(apkZipFile, classesZipEntry, outputDexZipFile, dexFileName);
                        isValidZipFile = ZipUtil.validZipFile(outputDexZipFile);
                        StringBuilder sb = new StringBuilder("Extraction ");
                        Log.i("PluginExtractor", sb
                                .append(isValidZipFile ? "success" : "failed")
                                .append(" - length ")
                                .append(outputDexZipFile.getAbsolutePath())
                                .append(": ")
                                .append(outputDexZipFile.length())
                                .toString());

                        if (!isValidZipFile) {
                            outputDexZipFile.delete();
                            if (outputDexZipFile.exists()) {
                                Log.w("PluginExtractor", "Failed to delete corrupted plugin dex \'" + outputDexZipFile.getPath() + "\'");
                            }
                        }
                        continue;
                    }

                    if (!isValidZipFile) {
                        throw new IOException("Could not create zip file " + outputDexZipFile.getAbsolutePath() + " for plugin dex (" + index + ")");
                    }
                    index++;
                    classesZipEntry = apkZipFile.getEntry("classes" + index + ".dex");
                    break;
                }
            }
            return outputDexZipList;
        } catch (Throwable v0) {
            return outputDexZipList;
        } finally {
            try {
                apkZipFile.close();
            } catch (IOException ioe) {
                Log.w("PluginExtractor", "Failed to close resource", ioe);
            }
        }
    }

    public static void saveUpdateInfo(Context context, String packageName, long lastModified, long crc, int dexSize) {
        SharedPreferences.Editor eidtor = PluginUtil.getPluginPreferences(context).edit();
        StringBuilder sb = new StringBuilder();
        sb.append(lastModified).append("|").append(crc).append("|").append(dexSize);
        eidtor.putString(packageName, sb.toString());
        PluginUtil.apply(eidtor);
    }

    public static boolean shouldPerformExtraction(Context context, String packageName, File apkFile, long currentCrc) {
        long saveTime = 0L;
        long saveCrc = 0L;
        String pluginInfo = PluginUtil.getPluginPreferences(context).getString(packageName, "");
        if (!TextUtils.isEmpty(pluginInfo)) {
            try {
                String[] segments = pluginInfo.split("\\|");
                saveTime = Long.parseLong(segments[0]);
                saveCrc = Long.parseLong(segments[1]);
            } catch (Exception e) {
            }
        }
        long currentTime = getLastModified(apkFile);
        if (Logger.debug()) {
            Logger.d("Plugin", "saveTime = " + saveTime);
            Logger.d("Plugin", "currentTime = " + currentTime);
            Logger.d("Plugin", "saveCrc = " + saveCrc);
            Logger.d("Plugin", "currentCrc = " + currentCrc);
        }
        return saveTime != currentTime || saveCrc != currentCrc;
    }

    public static long getLastModified(File file) {
        long lastModified = file.lastModified();
        if (lastModified == -1) {
            --lastModified;
        }
        return lastModified;
    }

    public static List<File> loadExistDexZipList(Context context, String packageName, File arg8, File arg9) throws IOException {
        Log.i("PluginExtractor", "loading existing plugin dex files");
        String dexFileName = arg8.getName() + ".classes";
        String v0 = PluginUtil.getPluginPreferences(context).getString(packageName, "");
        int dexSize;
        if (!TextUtils.isEmpty(v0)) {
            try {
                dexSize = Integer.parseInt(v0.split("\\|")[2]);
            } catch (Exception e) {
                dexSize = 1;
            }
        } else {
            dexSize = 1;
        }
        ArrayList<File> dexZipList = new ArrayList(dexSize);
        int index = 1;
        while (true) {
            if (index > dexSize) {
                return dexZipList;
            }
            File dexZipFile = new File(arg9, dexFileName + index + ".zip");
            if (!dexZipFile.isFile()) {
                throw new IOException("Missing extracted plugin dex file \'" + dexZipFile.getPath() + "\'");
            }

            dexZipList.add(dexZipFile);
            if (!ZipUtil.validZipFile(dexZipFile)) {
                Log.i("PluginExtractor", "Invalid zip file: " + dexZipFile);
                throw new IOException("Invalid ZIP file.");
            }
            ++index;
        }
    }

    public static Pair<List<File>, File> extractPlugin(Context context, ApplicationInfo info, File outputDexDir, File outputLibDir) throws IOException {
        Log.i("PluginExtractor", "MultiDexExtractor.load(" + info.sourceDir + ", false)");
        File apkFile = new File(info.sourceDir);
        File sourceFile = new File(info.sourceDir);
        long crc = FileUtils.getCrc(sourceFile);
        if (crc == -1) {
            --crc;
        }

        String packageName = info.packageName;
        if (Logger.debug()) {
            Logger.d("Plugin", "load package = " + packageName);
        }

        List<File> outputDexZipList;
        if (PluginExtractor.shouldPerformExtraction(context, packageName, sourceFile, crc)) {
            Log.i("PluginExtractor", "Detected that extraction must be performed.");
            outputDexZipList = PluginExtractor.extractDex2ZipList(apkFile, outputDexDir);
            PluginLibExtractor.reloadLibs(apkFile, outputLibDir);
            PluginExtractor.saveUpdateInfo(context, packageName, PluginExtractor.getLastModified(apkFile), crc, outputDexZipList.size());
        } else {
            try {
                outputDexZipList = PluginExtractor.loadExistDexZipList(context, packageName, apkFile, outputDexDir);
            } catch (IOException ioe) {
                Log.w("PluginExtractor", "Failed to reload existing extracted plugin dex files, falling back to fresh extraction", ioe);
                outputDexZipList = PluginExtractor.extractDex2ZipList(apkFile, outputDexDir);
                PluginLibExtractor.reloadLibs(apkFile, outputLibDir);
                PluginExtractor.saveUpdateInfo(context, packageName, PluginExtractor.getLastModified(apkFile), crc, outputDexZipList.size());
            }
        }

        Log.i("PluginExtractor", "load found " + outputDexZipList.size() + " plugin dex files");
        return new Pair<>(outputDexZipList, outputLibDir);
    }


    public static Pair<List<File>, File> extractDexAndLib(Context context, ApplicationInfo info, File outputDexDir, File outputLibDir) {
        if (context != null && info != null) {
            Log.v("Plugin", "doing inject paths:" + info.sourceDir);
            try {
                return PluginExtractor.extractPlugin(context, info, outputDexDir, outputLibDir);
            } catch (Exception e) {
            }
        }
        return null;
    }
}
